﻿Imports System.Security
Imports System.Windows.Input
Imports System.Windows
Imports System.Windows.Controls
Imports System.Windows.Controls.Primitives
Imports System.Windows.Threading

Public Enum SearchMode
    Instant
    Regular
End Enum

Public Enum SearchBoxState
    Prompt
    Input
    Text
    Search
End Enum

<TemplatePart(Name:="PART_SearchText", Type:=GetType(TextBoxBase))> _
<TemplatePart(Name:="PART_SearchButton", Type:=GetType(ButtonBase))> _
<TemplatePart(Name:="PART_DropDownArrow", Type:=GetType(ToggleButton))> _
Public Class SearchBox
    Inherits Control
    Implements ICommandSource
#Region "Text Dependency Property"

    Public Property Text() As String
        Get
            Return DirectCast(GetValue(TextProperty), String)
        End Get
        Set(ByVal value As String)
            SetValue(TextProperty, value)
        End Set
    End Property

    Public Shared ReadOnly TextProperty As DependencyProperty = DependencyProperty.Register("Text", GetType(String), GetType(SearchBox), New FrameworkPropertyMetadata([String].Empty, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, New PropertyChangedCallback(AddressOf OnTextChanged)))

#End Region

#Region "Prompt Dependency Property"

    Public Property Prompt() As String
        Get
            Return DirectCast(GetValue(PromptProperty), String)
        End Get
        Set(ByVal value As String)
            SetValue(PromptProperty, value)
        End Set
    End Property

    Public Shared ReadOnly PromptProperty As DependencyProperty = DependencyProperty.Register("Prompt", GetType(String), GetType(SearchBox), New UIPropertyMetadata([String].Empty))

#End Region

#Region "Command Dependency Property"

    Public ReadOnly Property Command() As ICommand Implements ICommandSource.Command
        Get
            Return DirectCast(GetValue(CommandProperty), ICommand)
        End Get
        'Set(ByVal value As ICommand)
        '    SetValue(CommandProperty, value)
        'End Set
    End Property

    Public Shared ReadOnly CommandProperty As DependencyProperty = DependencyProperty.Register("Command", GetType(ICommand), GetType(SearchBox), New UIPropertyMetadata(Nothing))

#End Region

#Region "CommandParameter Dependency Property"

    Public ReadOnly Property CommandParameter() As Object Implements ICommandSource.CommandParameter
        Get
            Return DirectCast(GetValue(CommandParameterProperty), Object)
        End Get
        'Set(ByVal value As Object)
        '    SetValue(CommandParameterProperty, value)
        'End Set
    End Property

    Public Shared ReadOnly CommandParameterProperty As DependencyProperty = DependencyProperty.Register("CommandParameter", GetType(Object), GetType(SearchBox), New UIPropertyMetadata(Nothing))

#End Region

#Region "CommandTarget Dependency Property"

    Public ReadOnly Property CommandTarget() As System.Windows.IInputElement Implements ICommandSource.CommandTarget
        Get
            Return DirectCast(GetValue(CommandTargetProperty), IInputElement)
        End Get
        'Set(ByVal value As IInputElement)
        '    SetValue(CommandTargetProperty, value)
        'End Set
    End Property

    Public Shared ReadOnly CommandTargetProperty As DependencyProperty = DependencyProperty.Register("CommandTarget", GetType(IInputElement), GetType(SearchBox), New FrameworkPropertyMetadata(Nothing))

#End Region

#Region "CommandDelay Dependency Property"

    Public Property CommandDelay() As Duration
        Get
            Return DirectCast(GetValue(CommandDelayProperty), Duration)
        End Get
        Set(ByVal value As Duration)
            SetValue(CommandDelayProperty, value)
        End Set
    End Property

    Public Shared ReadOnly CommandDelayProperty As DependencyProperty = DependencyProperty.Register("CommandDelay", GetType(Duration), GetType(SearchBox), New FrameworkPropertyMetadata(New Duration(TimeSpan.FromMilliseconds(500.0)), New PropertyChangedCallback(AddressOf OnSearchCommandDelayChanged)))

#End Region

#Region "Mode Dependency Property"

    Public Property Mode() As SearchMode
        Get
            Return DirectCast(GetValue(ModeProperty), SearchMode)
        End Get
        Set(ByVal value As SearchMode)
            SetValue(ModeProperty, value)
        End Set
    End Property

    Public Shared ReadOnly ModeProperty As DependencyProperty = DependencyProperty.Register("Mode", GetType(SearchMode), GetType(SearchBox), New UIPropertyMetadata(SearchMode.Instant))

#End Region

#Region "State Dependency Property"

    Public Property State() As SearchBoxState
        Get
            Return DirectCast(GetValue(StateProperty), SearchBoxState)
        End Get
        Set(ByVal value As SearchBoxState)
            SetValue(StateProperty, value)
        End Set
    End Property

    Public Shared ReadOnly StateProperty As DependencyProperty = DependencyProperty.Register("State", GetType(SearchBoxState), GetType(SearchBox), New UIPropertyMetadata(SearchBoxState.Prompt))

#End Region

    Private delayTimer As DispatcherTimer

    Shared Sub New()
        FrameworkElement.DefaultStyleKeyProperty.OverrideMetadata(GetType(SearchBox), New FrameworkPropertyMetadata(GetType(SearchBox)))
    End Sub

    Public Sub New()
        delayTimer = New DispatcherTimer(DispatcherPriority.Background, Dispatcher)
        delayTimer.Interval = CommandDelay.TimeSpan
        AddHandler delayTimer.Tick, AddressOf OnCommandDelayTimerTick
    End Sub

    Public Overrides Sub OnApplyTemplate()
        MyBase.OnApplyTemplate()
        Dim searchText = TryCast(GetTemplateChild("PART_SearchText"), TextBoxBase)
        If searchText IsNot Nothing Then
            AddHandler searchText.IsKeyboardFocusWithinChanged, AddressOf OnTextBoxIsKeyboardFocusedChanged
            AddHandler searchText.KeyDown, AddressOf OnTextBoxKeyDown
        End If
        Dim searchButton = TryCast(GetTemplateChild("PART_SearchButton"), ButtonBase)
        If searchButton IsNot Nothing Then
            AddHandler searchButton.Click, AddressOf OnSearchButtonClick
        End If
        Dim dropDownArrow = TryCast(GetTemplateChild("PART_DropDownArrow"), ToggleButton)
        If dropDownArrow IsNot Nothing Then
            AddHandler dropDownArrow.Click, AddressOf OnDropDownArrowClick
        End If
    End Sub

    Private Sub OnTextBoxIsKeyboardFocusedChanged(ByVal sender As Object, ByVal e As DependencyPropertyChangedEventArgs)
        Dim focused = CBool(If(e.NewValue, False))
        If focused Then
            OnTextBoxGotFocus(sender)
        Else
            OnTextBoxLostFocus()
        End If
    End Sub

    Private Sub OnTextBoxLostFocus()
        If State = SearchBoxState.Input Then
            State = SearchBoxState.Prompt
        End If
    End Sub

    Private Sub OnTextBoxGotFocus(ByVal sender As Object)
        If Not [String].IsNullOrEmpty(Text) Then
            Dim searchText = DirectCast(sender, TextBoxBase)
            searchText.SelectAll()

            State = SearchBoxState.Text
        Else
            State = SearchBoxState.Input
        End If
    End Sub

    Private Shared Sub OnTextChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        Dim searchBox = DirectCast(d, SearchBox)
        Dim text = DirectCast(e.NewValue, String)

        searchBox.State = If([String].IsNullOrEmpty(text), SearchBoxState.Input, SearchBoxState.Text)

        If searchBox.Mode = SearchMode.Instant Then
            searchBox.delayTimer.[Stop]()
            searchBox.delayTimer.Start()
        End If
    End Sub

    Private Sub OnTextBoxKeyDown(ByVal sender As Object, ByVal e As KeyEventArgs)
        If e.Key = Key.Escape AndAlso Mode = SearchMode.Instant Then
            ClearText(SearchBoxState.Input)
        ElseIf (e.Key = Key.[Return] OrElse e.Key = Key.Enter) AndAlso Mode = SearchMode.Regular Then
            If State = SearchBoxState.Search Then
                ClearText(SearchBoxState.Input)
            Else
                ExecuteCommand()
            End If
        End If
    End Sub

    Private Sub ClearText(ByVal state__1 As SearchBoxState)
        If Not [String].IsNullOrEmpty(Text) Then
            Text = [String].Empty
        End If
        State = state__1
    End Sub

    Protected Overrides Sub OnContextMenuOpening(ByVal e As ContextMenuEventArgs)
        If ContextMenu IsNot Nothing Then
            ContextMenu.IsOpen = False
        End If
        e.Handled = True
    End Sub

    Private Sub OnDropDownArrowClick(ByVal sender As Object, ByVal e As RoutedEventArgs)
        If ContextMenu Is Nothing OrElse Not ContextMenu.HasItems Then
            Return
        End If

        Dim dropDownArrow = DirectCast(sender, ToggleButton)

        If If(dropDownArrow.IsChecked, False) Then
            ContextMenu.PlacementTarget = dropDownArrow
            ContextMenu.Placement = PlacementMode.Bottom
            ContextMenuService.SetPlacement(dropDownArrow, PlacementMode.Bottom)
        End If
    End Sub

    Private Sub OnSearchButtonClick(ByVal sender As Object, ByVal e As RoutedEventArgs)
        If Not [String].IsNullOrEmpty(Text) Then
            If Mode = SearchMode.Instant OrElse State = SearchBoxState.Search Then
                ClearText(SearchBoxState.Prompt)
            Else
                ExecuteCommand()
            End If
        End If
    End Sub

    Private Shared Sub OnSearchCommandDelayChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
        Dim searchBox = DirectCast(d, SearchBox)
        If searchBox IsNot Nothing Then
            searchBox.delayTimer.Interval = DirectCast(e.NewValue, Duration).TimeSpan
            searchBox.delayTimer.[Stop]()
        End If
    End Sub

    Private Sub OnCommandDelayTimerTick(ByVal sender As Object, ByVal e As EventArgs)
        delayTimer.[Stop]()
        ExecuteCommand()
    End Sub

    Private Sub ExecuteCommand()
        ExecuteCommand(Me)
        If Mode = SearchMode.Regular Then
            State = SearchBoxState.Search
        End If
    End Sub

    <SecurityCritical()> _
    Private Shared Sub ExecuteCommand(ByVal commandSource As ICommandSource)
        Dim command = commandSource.Command
        If command IsNot Nothing Then
            Dim commandParameter = commandSource.CommandParameter
            Dim commandTarget = commandSource.CommandTarget
            Dim routedCommand = TryCast(command, RoutedCommand)
            If routedCommand IsNot Nothing Then
                If commandTarget Is Nothing Then
                    commandTarget = TryCast(commandSource, IInputElement)
                End If
                If routedCommand.CanExecute(commandParameter, commandTarget) Then
                    routedCommand.Execute(commandParameter, commandTarget)
                End If
            ElseIf command.CanExecute(commandParameter) Then
                command.Execute(commandParameter)
            End If
        End If
    End Sub
End Class

